/*
 * Copyright (c) 2014, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.omorphdb.core.storage;

import java.io.IOException;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.omorphdb.core.exception.WrappedIOException;
import org.omorphdb.core.index.ChainedIndex;
import org.omorphdb.core.index.IndexType;
import org.omorphdb.core.index.Indexes;
import org.omorphdb.core.io.Resource;
import org.omorphdb.core.io.ResourceFinder;
import org.omorphdb.core.storage.document.Document;
import org.omorphdb.core.storage.document.DocumentDescriptor;
import org.omorphdb.core.storage.document.FieldFactory;
import org.omorphdb.core.storage.statement.Result;
import org.omorphdb.core.value.Value;

/**
 * Defines a layer wrapping all functionality that read write and search
 * documents within a given {@link Resource}, providing access to stored
 * documents.
 * 
 * @author Marco Tamburelli
 */
public class Statements
{
	/**
	 * Iterator browsing documents returned by an iterator over pointers. In
	 * other words extends a pointers iterator to pointed documents.
	 */
	private final class IndexSearchIterator implements Iterator<Document>
	{
		private final Iterator<Long> iterator;

		private IndexSearchIterator(Iterator<Long> iterator)
		{
			this.iterator = iterator;
		}

		@Override
		public void remove()
		{
			throw new UnsupportedOperationException();
		}

		@Override
		public Document next()
		{
			try
			{
				return Statements.this.reader.get(iterator.next());
			}
			catch (IOException e)
			{
				throw new WrappedIOException(e);
			}
		}

		@Override
		public boolean hasNext()
		{
			return iterator.hasNext();
		}
	}

	/**
	 * Iterator that browse documents matching the provided parameters. It uses
	 * the sequential scan technique to fetch required elements from the storage
	 */
	private final class SequentialScanIterator<T> implements Iterator<Document>
	{
		private final int classID;
		private final String field;
		private final Value<T> key;

		private Document doc;

		private Document next;
		private boolean hasNext;

		private WrappedIOException exception;

		SequentialScanIterator(int classID, String field, Value<T> key)
		{
			this.classID = classID;
			this.field = field;
			this.key = key;

			try
			{
				this.doc = Statements.this.reader.getFirst();
				tryNext();
			}
			catch (IOException e)
			{
				this.exception = new WrappedIOException(e);
			}
		}

		private void tryNext()
		{
			while (this.doc != null)
			{
				DocumentDescriptor descr = doc.getDescriptor();
				Value<T> value = this.doc.get(this.field);

				try
				{
					if (descr.getClassID() == this.classID && this.key.equals(value))
					{
						this.next = this.doc;
						this.hasNext = true;

						return;
					}
				}
				finally
				{
					try
					{
						this.doc = Statements.this.reader.getNext();
					}
					catch (IOException e)
					{
						this.exception = new WrappedIOException(e);
					}
				}
			}

			this.next = null;
			this.hasNext = false;
		}

		@Override
		public void remove()
		{
			throw new UnsupportedOperationException();
		}

		@Override
		public Document next()
		{
			if (this.hasNext) try
			{
				return this.next;
			}
			finally
			{
				tryNext();
			}
			else throw new NoSuchElementException();
		}

		@Override
		public boolean hasNext()
		{
			if (this.exception == null) return this.hasNext;
			else throw this.exception;
		}
	}

	private final FieldFactory fieldFactory;
	private final Indexes indexes;

	private final DocumentReader reader;
	private final DocumentWriter writer;

	/**
	 * Constructor.
	 * 
	 * @param fieldFactory
	 * @param indexes
	 * @param resourceFinder
	 * 
	 * @throws IOException
	 */
	public Statements(FieldFactory fieldFactory, Indexes indexes, ResourceFinder resourceFinder) throws IOException
	{
		this.fieldFactory = fieldFactory;
		this.indexes = indexes;

		this.reader = new DocumentReader(this.fieldFactory, resourceFinder);
		this.writer = new DocumentWriter(this.fieldFactory, resourceFinder);
	}

	/**
	 * Creates an empty document from a provided class decided by a class ID.
	 * 
	 * @param classID
	 * @return
	 */
	public Document createDocument(int classID)
	{
		return DocumentImpl.createDocument(classID, this.fieldFactory);
	}

	/**
	 * Makes the changes of a provided document permanent.
	 * 
	 * @param doc
	 * @throws IOException
	 */
	public void saveDocument(Document doc) throws IOException
	{
		this.writer.save((DocumentImpl) doc);
	}

	/**
	 * Search a document by class ID, a field, and a value for the provided
	 * field. <br>
	 * Returns an iterator over matched elements.
	 * 
	 * @param classID
	 * @param field
	 * @param key
	 * @return
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	public <T> Result searchEquals(final int classID, final String field, final Value<T> key) throws IOException
	{
		EnumMap<IndexType, ChainedIndex<?>> idxMap = this.indexes.getIndexes(classID, field);
		final IndexType usedIndex;

		ChainedIndex<T> idx = (ChainedIndex<T>) idxMap.get(IndexType.HASH);
		if (idx == null)
		{
			idx = (ChainedIndex<T>) idxMap.get(IndexType.SORTED);

			if (idx == null) return new Result()
			{
				@Override
				public Iterator<Document> iterator()
				{
					return new SequentialScanIterator<T>(classID, field, key);
				}

				@Override
				public IndexType getUsedIndexType()
				{
					return null;
				}
			};
			else usedIndex = IndexType.SORTED;
		}
		else usedIndex = IndexType.HASH;

		final Iterator<Long> iterator = idx.search(key.get());

		return new Result()
		{
			@Override
			public Iterator<Document> iterator()
			{
				return new IndexSearchIterator(iterator);
			}

			@Override
			public IndexType getUsedIndexType()
			{
				return usedIndex;
			}
		};
	}
}
